(ns clojure-server-commit.core
  (:require [clojure.string :as str]
            [clojure.edn :as edn])
  (:import (java.util.concurrent.locks ReentrantLock)
           (java.net Socket SocketException InetAddress ServerSocket)
           (clojure.lang LineNumberingPushbackReader)
           (java.io InputStreamReader BufferedWriter OutputStreamWriter)))

(set! *warn-on-reflection* true)

(def ^:dynamic *session* nil)

(defonce ^:private lock (ReentrantLock.))
;; 存储服务器集合
(defonce ^:private servers {})

(defmacro ^:private with-lock
  [lock-expr & body]
  `(let [lockee# ~(with-meta lock-expr {:tag 'java.util.concurrent.locks.ReentrantLock})]
     (.lock lockee#)
     (try
       ~@body
       (finally
         (.unlock lockee#)))))

;; 定义一个thread宏 用于简化线程执行
;; 该线程只执行相关函数 不进行交流
(defmacro ^:private thread
  ;; 线程名称 是否守护 执行体
  [^String name daemon & body]
  ;; 执行
  `(doto (Thread. (fn [] ~@body) ~name)
     (.setDaemon ~daemon)
     (.start)))

;; 检查必要参数是否存在
(defn- required
  "如果可选项不包含指定属性就报错"
  [opts prop]
  (when (nil? (get opts prop))
    (throw (ex-info (str "服务端套接字丢失关键属性" prop) opts))))

(defn- validate-opts
  "验证服务端配置可选项"
  [{:keys [name port accept] :as opts}]
  ;; 验证配置是否包含名称,端口,处理器
  (doseq [prop [:name :port :accept]]
    (required opts prop))
  ;; 验证端口有效性
  (when (or (not (integer? port))
            (not (< -1 port 65633)))
    (throw (ex-info (str "非法的服务器端口: " port) opts))))

(defn- accept-connection
  "开始接受请求函数 被客户端线程所调用
  conn 客户端套接字
  name 服务端名称
  client-id 客户端标识
  in out err 流
  accept 处理器函数符号
  args 处理器函数"
  [^Socket conn name client-id in out err accept args]
  (try
    (binding [*in* in
              *out* out
              *err* err
              *session* {:server-name name :client client-id}]
      ;; 把该客户端存入集合中
      (with-lock lock
                 (alter-var-root #'servers assoc-in [name :sessions client-id]))
      ;; 确保函数的命令空间被加载
      (require (symbol (namespace accept)))
      ;; 解析出函数的完整命名空间
      (let [accept-fn (resolve accept)]
        (apply accept-fn args)))
    ;; 如果报错的话
    (catch SocketException _disconnect)
    (finally
      ;; 删除相关错误的socket
      (with-lock lock
                 (alter-var-root #'servers update-in [name :sessions] dissoc client-id))
      (.close conn))))

(defn start-server
  "根据选项开启服务
  :address host地址 字符串 默认是127.0.0.1
  :port 端口 整数 必须
  :name 名称 必须
  :accept 处理逻辑函数的全命名空间 必须
  :args 向量参数
  :bind-err 重定向socket的错误到标准错误
  :server-daemon 服务端处理线程是否守护
  :client-daemon 客户端处理线程是否守护
  返回服务端套接字"
  [opts]
  ;; 验证参数
  (validate-opts opts)
  ;; 提取出所有参数
  ;; 并且带有默认值
  ;; 构建一个服务端socket
  (let [{:keys [address port name accept args bind-err server-daemon client-daemon]
         :or   {bind-err      true
                server-daemon true
                client-daemon true}} opts
        address (InetAddress/getByName address)             ;; nil 返回 127.0.0.1
        ^ServerSocket socket (ServerSocket. port 0 address)]
    ;; 修改服务器端socket的集合
    (with-lock lock
               ;; 增加到服务器集合中
               (alter-var-root #'servers assoc name {:name name :socket socket :sessions {}}))
    ;; 新线程处理请求
    (thread
      (str "Clojure 服务 " name)
      server-daemon
      (try
        ;; 客户端请求数量
        (loop [client-counter 1]
          ;; 当服务端套接字没有关闭时 处理请求
          (when (not (.isClosed socket))
            (try
              ;; 获取请求套接字
              (let [^Socket conn (.accept socket)
                    ;; 基于行的读取
                    in (LineNumberingPushbackReader. (InputStreamReader. (.getInputStream conn)))
                    out (BufferedWriter. (OutputStreamWriter. (.getOutputStream conn)))
                    ;; 转换成客户端id
                    client-id (str client-counter)]
                (thread
                  (str "Clojure 连接" name " " client-id)
                  client-daemon
                  (accept-connection conn name client-id in out (if bind-err out *err*) accept args)))
              (catch SocketException _disconnect))
            ;; 客户端增加 为下个客户端socket做好准备
            (recur (inc client-counter))))
        (finally
          ;; 修改的时候加锁
          (with-lock lock
                     (alter-var-root #'servers dissoc name)))))
    ;; 返回服务器端套件
    socket))

(defn stop-server
  "停止服务器
  指定名称
  或者从会话中获取
  true 成功停止
  nil 没有找到
  发生错误会报错"
  ([]
   (stop-server (:server *session*)))
  ([name]
   (with-lock lock
              ;; 获取socket
              (let [server-socket ^ServerSocket (get-in servers [name :socket])]
                (when server-socket
                  ;; 清除socket
                  (alter-var-root #'servers dissoc name)
                  ;; 关闭
                  (.close server-socket)
                  true)))))

(defn- parse-props
  "解析 clojure.server.* 的系统配置属性"
  [props]
  (reduce
    (fn [acc [^String k ^String v]]
      ;; 分割clojure.server.*
      (let [[k1 k2 k3] (str/split k #"\.")]
        ;; 提取出要选取出来的配置
        (if (and (= k1 "clojure") (= k2 "server"))
          ;; 合并进acc
          (conj acc (merge {:name k3} (edn/read-string v)))
          acc)))
    [] props))

(defn start-servers
  "开启所有在系统配置里面指定的服务器"
  [system-props]
  (doseq [server (parse-props system-props)]
    (start-server server)))

(defn stop-servers
  "停止所有服务端socket"
  []
  (with-lock lock
             (doseq [name (keys servers)]
               (future (stop-server name)))))

(defn repl-init
  "初始化repl 并且加载repl所需要的命名空间"
  []
  (in-ns 'user)
  (apply require clojure.main/repl-requires))